<!--
Copyright 2012 The Polymer Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!--
/**
 * @module Polymer Elements
 */
 /**
  * g-panels is used to display a list of elements that can be selected.
  * The attribute "selected" indicates which element is being selected.
  * Typically only the selected element is displayed. By default, elements
  * inside g-panels fit to its size.
  *
  * When the selected panel changes, a transition occurs between the current
  * and new selection. The transition property specifies an element that
  * manages the selected transition. The 'keyframe' transition uses css
  * animations to manage trasnitions. A number of different classes can be
  * applied to g-panels to customize the css animations used for a keyframe
  * transition:
  *
  * * g-panels-hslide, g-panels-vslide
  * * g-panels-hslideover, g-panels-vslideover
  * * g-panels-explode
  * * g-panels-scale-slide
  * * g-panels-fly-up-right
  *
  * The 'flow' transition moves horizontally to reveal content.
  *
  * Example:
  *
  *     <g-panels selected="bar" transition="keyframe" class="g-panels-explode">
  *       <section name="foo">Panel 1</section>
  *       <section name="bar">Panel 2</section>
  *       <section name="baz">Panel 3</section>
  *    </g-panels>
  *
  * @class g-panels
  */
 /**
  * Fired when an element is selected via tap.
  *
  * @event select
  * @param {Object} inDetail
  *   @param {Object} inDetail.selected the selected element
  *   @param {Object} inDetail.index the index in the list of elements
  */
 /**
  * Fired when an element is about to be selected. Call the preventSelect
  * to prevent the selection.
  * @event canSelect
  * @param {Object} inDetail
  *   @param {Object} inDetail.selected the selected element
  *   @param {Object} inDetail.item the selected element
  *   @param {Function} inDetail.preventSelect call to prevent selection
  */
  -->
<link rel="import" href="panel-transitions/g-panel-transition.html">
<link rel="import" href="panel-transitions/g-keyframe-panel-transition.html">
<link rel="import" href="panel-transitions/g-flow-panel-transition.html">
<polymer-element name="g-panels" attributes="transition selected index autoselect" 
         on-keydown="keydownHandler">
<template>
  <link rel="stylesheet" href="css/g-panels.css" />
  <link rel="stylesheet" polymer-scope="global" href="css/g-panels-global.css" />
  <link rel="stylesheet" polymer-scope="controller" href="css/g-panels-controller.css" />
  <content id="content" select="*"></content>
</template>
<script>
  (function() {
    // TODO(sorvell): promote this and figure out canonical way to do this;
    // and support contenteditable.
    var documentIsEditing = function() {
      var a = document.activeElement;
      if (a.tagName == "INPUT" || a.tagName == "SELECT" || a.tagName == "TEXTAREA") {
        return true;
      }
    }
    
    // TODO(sorvell): need key table
    var RIGHT_ARROW_KEY = 39;
    var LEFT_ARROW_KEY = 37;
    
    var element = this;
    
    Polymer('g-panels', {
      /**
       * The type of transition g-panels should use to change between selected
       * panels. The transition property corresponds to an element that is used
       * to manage panel transitions. If it's not a custom element name,
       * transition is converted into g-<transition>-panel-transition.
       *
       * @attribute transition
       * @type string
       * @default keyframe
       *
  
       */
      transition: 'keyframe',
      /**
       * The selected panel to select. Panels are identify by either their name
       * or id attributes. If this value is unset, then when g-panels is created
       * its first panel will be selected.
       *
       * @attribute selected
       * @type string
       * @default null
       */
      selected: null,
      index: -1,
      autoSelect: false,
      defaultTransition: 'keyframe',
      _canTransition: false,
      ready: function() {
        this.initPanels();
        this.observeChildren();
        if (!this.hasAttribute('tabindex')) {
          this.tabIndex = -1;
        }
      },
      inserted: function() {
        this.installControllerStyles(this, element);
        this.enableTransitions(false);
        this.flushChildrenMutations();
        if (this.index === -1 && this.selected === null) {
          this.indexChanged();
        }
        this.enableTransitions(true);
      
      },
      initPanels: function() {
        this.getPanels().forEach(function(p, i) {
          this.initPanel(p, i);
        }, this);
      },
      observeChildren: function() {
        this.boundHandleMutations = this.handleMutations.bind(this);
        this.childrenObserver = new MutationObserver(this.boundHandleMutations);
        this.childrenObserver.observe(this, {childList: true});
      },
      handleMutations: function(mutations) {
        Array.prototype.forEach.call(mutations, function(m) {
          if (m.removedNodes) {
            this.nodesRemoved(m.removedNodes);
          }
          if (m.addedNodes) {
            this.nodesAdded(m.addedNodes);
          }
        }, this);
      },
      flushChildrenMutations: function() {
        this.boundHandleMutations(this.childrenObserver.takeRecords());
      },
  
      /**
       * Extendors can implement initPanel to initialize the panels in a g-panels.
       * @method initPanel
       * @param inPanel The panel to initialize.
       * @param inIndex The index of the panel.
       */
      initPanel: function(inPanel, inIndex) {
        this.ensureTransition();
        this.transitionNode.setupPanel(inPanel, inIndex);
      },
      teardownPanel: function(inPanel) {
        this.ensureTransition();
        this.transitionNode.teardownPanel(inPanel);
      },
      indexChanged: function(inOldValue) {
        var i = this.clampIndex(this.index);
        if (!this.canSelect(i)) {
          this.index = inOldValue;
          //console.log('cannot select')
          return;
        }
        if (i != this.index) {
          this.index = i;
        }
        if (this.index != this.lastIndex) {
          var fromPanel = this.panelAtIndex(this.lastIndex),
              toPanel = this.panelAtIndex(this.index),
              forward = Boolean(this.index > this.lastIndex);
          this.ensureTransition();
          if (this.canTransition(fromPanel, toPanel, forward)) {
            this.beginTransition(fromPanel, toPanel, forward);
          } else {
            this.finishTransition(fromPanel, toPanel, forward);
          }
          this.lastIndex = this.index;
          this.selected = this.getSelectedValue();
        }
      },
      ensureTransition: function() {
        if (!this.transitionNode) {
          this.transitionChanged();
        }
      },
      canSelect: function(inIndex) {
        var canSelect = true;
        // TODO(sorvell): dirtyCheck before we sent the canSelect event since
        // it's likely that the decision needs to rely on binding state.
        // consider doing this in g-component.send.
        dirtyCheck();
        this.fire('canselect', {
          index: inIndex, 
          selected: this.selectedValueForIndex(inIndex),
          preventSelect: function() {
            canSelect = false;
          }
        });
        return canSelect;
      },
      clampIndex: function(inIndex) {
        var i = isNaN(inIndex) ? 0 : inIndex;
        return Math.max(0, Math.min(this.count-1, i));
      },
      getSelectedValue: function() {
        var selected = this.getSelectedPanel();
        return this.selectedValueForPanel(selected);
      },
      selectedValueForPanel: function(inPanel) {
        var name = inPanel && (inPanel.getAttribute('id') ||
            inPanel.getAttribute('name'));
        return name || ('' + this.indexOf(inPanel));
      },
      selectedValueForIndex: function(inIndex) {
        var panel = this.panelAtIndex(inIndex);
        return this.selectedValueForPanel(panel);
      },
      // selected is the name property of the panel to select
      selectedChanged: function(inOldValue) {
        var index = this.indexOfPropValue('id', this.selected);
        index = index >= 0 ? index : this.indexOfPropValue('name', this.selected);
        index = index >= 0 ? index : Number(this.selected);
        if (index >= 0) {
          this.index = index;
        }
      },
      transitionChanged: function(inOldValue) {
        var userTransition = this.findTransitionNode();
        if (userTransition) {
          this.transitionNode = userTransition;
        } else {
          if (this.transitionNode) {
            if (this.transitionNode.localName == 
                this.tagNameForTransition(this.transition)) {
              return;
            }
            this.transitionNode.teardown();
          }
          this.transitionNode = this.makeTransitionNode(this.transition);
          this.setAttribute('transition', this.transition);
        }
        this.transitionNode.setup(this);
        this.refresh();
      },
      findTransitionNode: function() {
        var n$ = this.$.content.getDistributedNodes();
        for (var i=0, n; n=n$[i]; i++) {
          if (this.isTransitionNode(n)) {
            return n;
          }
        };
      },
      isTransitionNode: function(inNode) {
        return inNode && (inNode.nodeType == Node.ELEMENT_NODE) &&
          inNode.localName.match(/^g-.*panel-transition$/);
      },
      makeTransitionNode: function(inName) {
        var name = this.tagNameForTransition(inName);
        var transition = document.createElement(name);
        return transition.setup ? transition :
            this.makeTransitionNode(this.defaultTransition);
      },
      tagNameForTransition: function(inName) {
        return 'g-' + inName + (inName ? '-' : '') + 'panel-transition';
      },
      indexOfPropValue: function(inProp, inValue) {
        var c$ = this.getPanels();
        for (var i=0, c; c=c$[i]; i++) {
          if (c.getAttribute(inProp) == inValue) {
            return i;
          }
        }
        return -1;
      },
      panelAtName: function(inName) {
        return this.panelAtIndex(this.indexOfName(inName));
      },
      canTransition: function(inFrom, inTo, inForward) {
        return this._canTransition && Boolean(this.transitionNode && inFrom && inTo &&
            this.transitionNode.canTransition(inFrom, inTo, inForward));
      },
      beginTransition: function(inFrom, inTo, inForward) {
        if (this.transitionNode) {
          this.transitionNode.stop();
        }
        //console.time('transition: ' + this.transition + ', ' + this.className);
        requestAnimationFrame(function() {
          this.enablePanelShowing(inFrom, true);
          this.enablePanelShowing(inTo, true);
          if (this.showByZ) {
            this.applyZ(inFrom, this.topZ);
            this.applyZ(inTo, this.topZ - 1);
          }
          this.transitionNode.go(inFrom, inTo, inForward);
        }.bind(this));
      },
      finishTransition: function(inFrom, inTo, inForward) {
        if (!this.canTransition(inFrom, inTo, inForward)) {
          this.transitionNode.refresh();
        }
        this.ensureSelectedShowing();
        //console.timeEnd('transition: ' + this.transition + ', ' + this.className);
        this.asyncFire('select', {selected: this.selected, index: this.index});
      },
      keydownHandler: function(e) {
        if (this.autoselect && !documentIsEditing()) {
          var oldIndex = this.index;
          if (e.keyCode == RIGHT_ARROW_KEY) {
            this.next();
          } else if (e.keyCode == LEFT_ARROW_KEY) {
            this.previous();
          }
          // TODO(sorvell): dirtyCheck so we know syncronously if index changed.
          dirtyCheck();
          if (this.index != oldIndex) {
            e.cancelBubble = true;
            this.focus();
          }
        }
      },
      refresh: function() {
        if (this._canTransition && this.transitionNode && 
            this.transitionNode.canTransition()) {
          this.transitionNode.refresh();
          this.ensureSelectedShowing();
        }
      },
      ensureSelectedShowing: function() {
        if (this.transitionNode.highlander) {
          this.getPanels().forEach(function(p, i) {
            if (this.showByZ) {
              this.applyZ(p, i == this.index ? this.topZ : 0);
            } else {
              this.enablePanelShowing(p, i == this.index);
            }
          }, this);
        }
      },
      // TODO(sorvell): this is an experimental mode where panels are 
      // shown by altering z-index... will probably either be removed
      // or used exclusively.
      showByZ: false,
      topZ: 2,
      applyZ: function(inNode, inZ) {
        inNode.style.zIndex = inZ;
      },
      enableTransitions: function(inEnable) {
        return this._canTransition = inEnable;
      },
      // note: cannot use hidden attr for this since it's trumped by display
      // setting e.g. hidden + display: -webkit-flex == showing.
      enablePanelShowing: function(inPanel, inEnable) {
        if (inPanel) {
          inPanel.style.display = inEnable ? null : 'none';
        }
      },
      /**
       *
       * Get the panel elements inside the g-panels.
       *
       * @method getPanels
       * @returns {Array}
       */
      getPanels: function() {
        var n$ = this.$.content.getDistributedNodes();
        return Array.prototype.filter.call(n$, this.nodeIsPanel, this);
      },
      nodeIsPanel: function(node) {
        var excluded = ['style', 'template'];
        return (node.nodeType == Node.ELEMENT_NODE) && 
          (excluded.indexOf(node.localName) < 0) && 
          !this.isTransitionNode(node);
      },
      /**
       * The number of panel elements inside the g-panels
       *
       * @attribute count
       * @returns {Number}
       */
      get count() {
        return this.getPanels().length;
      },
      /**
       * Returns the currently selected panel
       * @method getSelectedPanel
       * @returns {Object}
       */
      getSelectedPanel: function() {
        return this.getPanels()[this.index];
      },
      /**
       *  Returns the index of given panel element.
       *
       * @method indexOf
       * @param inPanel
       * @returns {Number}
       */
      indexOf: function(inPanel) {
        return this.getPanels().indexOf(inPanel);
      },
      /**
       * Returns the panel element at the given index in the list of panels
       *
       * @method panelAtIndex
       * @param inIndex
       * @returns {Object}
       */
      panelAtIndex: function(inIndex) {
        return this.getPanels()[inIndex];
      },
      /**
       * Selects the next panel in the list of panel elements.
       *
       * @method next
       */
      next: function() {
        this.index++;
      },
      /**
       * Selects the previous panel in the list of panel elements.
       *
       * @method previous
       */
      previous: function() {
        this.index--;
      },
      /**
       * Toggles the selected panel between two values
       *
       * @method toggleBetween
       * @param inA
       * @param inB
       */
      toggleBetween: function(inA, inB) {
        this.selected = this.selected == inA ? inB : inA;
      },
      // deprecrated
      panelAdded: function(inPanel) {
        this.flushChildrenMutations();
      },
      nodesAdded: function(nodes) {
        var panels = this.getPanels();
        this.ensureTransition();
        Array.prototype.forEach.call(nodes, function(node) {
          var i = panels.indexOf(node);
          if (i >= 0) {
            this.initPanel(node, i);
          }
        }, this);
        this.refresh();
      },
      nodesRemoved: function(nodes) {
        Array.prototype.forEach.call(nodes, function(node) {
          if (this.nodeIsPanel(node)) {
            this.teardownPanel(node);
          }
        }, this);
      }
    });
  })();
</script>
</polymer-element>